/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.console.config;


import org.jline.reader.LineReader;
import org.openislands.oi.console.config.annotation.CommandArgs;
import org.openislands.oi.console.config.annotation.CommandParam;
import org.openislands.oi.console.config.bean.BeanContainer;
import org.openislands.oi.console.config.bean.CommandGroupContext;
import org.openislands.oi.console.config.bean.SingleCommandGroupContext;
import org.openislands.oi.console.exception.CommandException;
import org.openislands.oi.console.exception.ExitException;
import org.openislands.oi.console.exception.InvokeException;
import org.openislands.oi.console.exception.ParamException;
import org.openislands.oi.console.utils.ParameterUtils;
import org.openislands.oi.constant.Dict;
import org.openislands.oi.error.RemoteCallException;
import org.openislands.oi.rpc.base.*;
import org.openislands.oi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

public class Actuator {
    private static final Logger log = LoggerFactory.getLogger(Actuator.class);

    /***
     * Command group context. See {@link #actuatorInit()}
     */
    private static CommandGroupContext commandGroupContext;
    private static SingleCommandGroupContext singleCommandGroupContext;

    public static void start() {
        try {
            LineReader lineReader = LineReaderConfig.createLineReader();
            actuatorInit();

            end:
            {
                while (true) {
                    try {
                        // read input line
                        String inputValue = lineReader.readLine(LineReaderConfig.prompt);
                        if (inputValue != null && !(inputValue = inputValue.trim()).isEmpty()) {
                            try {
                                log.debug("input: => {}", inputValue);
                                // parse input command
                                CommandParser.parse(inputValue).execute();
                            } catch (CommandException e) {
                                LineReaderConfig.tips();
                            } catch (ExitException e) {
                                break end;
                            } catch (Exception e) {
                                OIConsole.console("execute error : " + e.getMessage());
                            }
                        }
                    } catch (InvokeException e) {
                        OIConsole.console("warning : " + e.getMessage());
                    } catch (Exception e) {
                        log.error(e.getMessage(), e);
                        break end;
                    }
                }
            }
        } catch (Exception e) {
            log.error("LineReader initialization failure, " + e.getMessage());
        }
        actuatorEnd();
        System.exit(0);
    }

    public static void actuatorInit() {
        commandGroupContext = BeanContainer.getBean(CommandGroupContext.class);
        singleCommandGroupContext = BeanContainer.getBean(SingleCommandGroupContext.class);
    }

    private static void actuatorEnd() {
        LineReaderConfig.end();
    }

    public static class CommandParser {

        /***
         * Parse command line
         * @param commandLine input command
         * @return Executor
         */
        public static CommandExecutor parse(String commandLine) {
            // use ' ' split
            List<String> commandList = Arrays.stream(commandLine.split(" ")).collect(Collectors.toList());
            return parse(commandList);
        }

        public static CommandExecutor parse(List<String> commandList) {
            // 0. base help
            String firstParam = commandList.get(0);
            if (LineReaderConfig.containsHelpCommand(firstParam)) {
                return new HelpExecutor(LineReaderConfig::help);
            } else if (LineReaderConfig.containsExitCommand(firstParam)) {
                return new ExitExecutor();
            }

            // 1. single command
            Method command = singleCommandGroupContext.getCommand(firstParam);
            if (command != null) {
                return parseSingleCommand(firstParam, command, commandList);
            }

            if (commandList.size() < 2) {
                OIConsole.console("not found " + firstParam + " command", 1);
                return new TipsExecutor();
            }

            // 2. command group command
            // parse command group
            final String groupName = firstParam;
            Map<String, Method> childCommandMap = commandGroupContext.getChildCommand(groupName);
            if (Objects.isNull(childCommandMap)) {
                throw new InvokeException("unknown " + groupName + " command");
            }

            // escape character
            commandList = escapeCharacter(commandList);

            // parse child command
            String childCommand = commandList.get(1);
            Method method = childCommandMap.get(childCommand);
            if (Objects.isNull(method)) {
                // group help
                if (LineReaderConfig.containsHelpCommand(childCommand)) {
                    return new HelpExecutor(() -> LineReaderConfig.printHelp(groupName, childCommandMap));
                }
                throw new InvokeException("unknown " + groupName + " " + childCommand + " command");
            }

            // is child command help
            if (commandList.size() > 2 && LineReaderConfig.containsHelpCommand(commandList.get(2))) {
                return new HelpExecutor(() -> LineReaderConfig.printHelp(groupName, childCommand, method));
            }

            return parseInvokeMethod(groupName, childCommand, method, commandList, 2);
        }

        private static CommandExecutor parseSingleCommand(String commandName, Method command, List<String> commandList) {
            // escape character
            commandList = escapeCharacter(commandList);

            // is command help
            if (commandList.size() > 1 && LineReaderConfig.containsHelpCommand(commandList.get(1))) {
                return new HelpExecutor(() -> LineReaderConfig.printHelp(StringUtils.EMPTY, commandName, command));
            }

            // singleCommandGroupContext.getCommandGroup()
            return parseInvokeMethod("groupName", commandName, command, commandList, 1);
        }

        private static CommandExecutor parseInvokeMethod(String groupName, String commandName, Method method,
                                                         List<String> params, int paramCutIndex) {
            try {
                // parameters check
                Object instance = BeanContainer.getBean(method.getDeclaringClass());
                List<String> stringList = params.subList(paramCutIndex, params.size());
                List<ParamVariable> paramVariableList = parseParameters(stringList, method.getParameters());

                // invoke method. get request wrapper
                Object[] args = paramVariableList.stream().map(ParamVariable::getValue).toArray();
                Object result = method.invoke(instance, args);
                log.debug("exec command :{} {}, args: {}, result: {}", groupName, commandName, args, result);
                if (result instanceof RequestWrapper) {
                    return new SimpleExecutor<>(((RequestWrapper<?, ?>) result), groupName, commandName, paramVariableList);
                } else {
                    return new HelpExecutor(() -> OIConsole.console(Objects.nonNull(result) ? result.toString() : "...", 1));
                }
            } catch (IllegalAccessException e) {
                log.error(e.getMessage(), e);
                throw new InvokeException("unable to call " + e.getMessage());
            } catch (InvocationTargetException e) {
                log.error(e.getMessage(), e);
                throw new InvokeException("invoke failed: " + (e.getMessage() == null ? e.getCause().getMessage() : e.getMessage()));
            }
        }

        /***
         * Handling characters that need to be escaped
         * @param commandList commands
         * @return result
         */
        private static List<String> escapeCharacter(List<String> commandList) {
            List<String> result = new ArrayList<>();
            // handle space
            String[] strings = commandList.toArray(new String[0]);
            for (int i = 1; i <= strings.length; i++) {
                String prevStr = strings[i - 1];
                if (prevStr.endsWith("\\")) { // [\ ]
                    if (i == strings.length) continue;
                    strings[i] = prevStr.substring(0, prevStr.length() - 1) + " " + strings[i];
                } else {
                    if (prevStr.isEmpty()) continue;
                    result.add(prevStr);
                }
            }

            return result;
        }

        /***
         * Parse command method parameters value
         * @param commandList   command parameter list
         * @param parameters    method parameter list
         * @return parameter value packing
         */
        private static List<ParamVariable> parseParameters(List<String> commandList, Parameter[] parameters) {
            Map<String, String> parameterValues = new HashMap<>();
            for (int i = 1; i < commandList.size(); i++) {
                parameterValues.put(commandList.get(i - 1), commandList.get(i));
            }

            List<ParamVariable> resultList = new ArrayList<>();
            for (Parameter parameter : parameters) {
                Class<?> parameterType = parameter.getType();
                // is the command parameter list
                if (parameter.isAnnotationPresent(CommandArgs.class) && parameterType == String[].class) {
                    resultList.add(new ParamVariable(parameter.getName(), parameter.getName(), commandList.toArray(new String[0])));
                    continue;
                }

                Object value;
                String paramName;
                boolean required = true;
                String defaultValue = "";
                CommandParam commandParam = parameter.getAnnotation(CommandParam.class);
                if (Objects.nonNull(commandParam)) {
                    defaultValue = commandParam.defaultValue();
                    required = commandParam.required();
                    paramName = LineReaderConfig.defaultParamName(parameter.getName(), commandParam);
                    try {
                        // parse input value
                        value = ParameterUtils.valueOf(parameterType, parameterValues.get(paramName));
                    } catch (Exception e) {
                        throw new InvokeException(e.getMessage() +
                                ". [parameter " + paramName + " type is " + parameterType.getSimpleName() + "]");
                    }
                } else {
                    paramName = LineReaderConfig.defaultParamName(parameter.getName());
                    value = ParameterUtils.valueOf(parameterType, parameterValues.get(paramName));
                }

                // required and no default value
                if (required && defaultValue.isEmpty() && Objects.isNull(value)) {
                    throw new ParamException(paramName);
                } else {
                    if (Objects.isNull(value) && !defaultValue.isEmpty()) {
                        value = ParameterUtils.valueOf(parameterType, defaultValue);
                    }
                }

                resultList.add(new ParamVariable(paramName, parameter.getName(), value));
            }

            return resultList;
        }
    }

    @FunctionalInterface
    public interface CommandExecutor {
        void execute() throws Exception;
    }

    public static class HelpExecutor implements CommandExecutor {
        private final CommandExecutor commandExecutor;

        public HelpExecutor(CommandExecutor commandExecutor) {
            this.commandExecutor = commandExecutor;
        }

        @Override
        public void execute() throws Exception {
            commandExecutor.execute();
        }
    }

    public static class ExitExecutor implements CommandExecutor {
        @Override
        public void execute() throws ExitException {
            throw new ExitException();
        }
    }

    public static class TipsExecutor implements CommandExecutor {
        @Override
        public void execute() {
            throw new CommandException("exit");
        }
    }

    public static class SimpleExecutor<T, R> implements CommandExecutor {
        private final RequestWrapper<T, R> requestWrapper;
        private final String groupName;
        private final String commandName;
        private final List<ParamVariable> paramVariableList;

        public SimpleExecutor(RequestWrapper<T, R> requestWrapper, String groupName, String commandName,
                              List<ParamVariable> paramVariableList) {
            this.requestWrapper = requestWrapper;
            this.groupName = groupName;
            this.commandName = commandName;
            this.paramVariableList = paramVariableList;
        }

        @SuppressWarnings("unchecked")
        @Override
        public void execute() {
            Type[] genericInterfaces = requestWrapper.getClass().getGenericInterfaces();
            final Type[] actualTypeArguments = ((ParameterizedType) genericInterfaces[0]).getActualTypeArguments();
            // get executor proxy
            ExecutorProxy executorProxy = ServiceManager.getExecutorProxy();
            Executor<T> executor = executorProxy.getExecutor((Class<T>) actualTypeArguments[0]);
            // set extended config
            String serviceName = ServiceManager.getServerMark(this.groupName);
            Extended<Class<?>, ItemExtended> extended = executor.extended();
            ItemExtended baseExtended = extended.get(BaseExtended.class, new BaseExtended());
            baseExtended.putItem(BaseExtended.ROUTE, ServiceManager.getRoute(serviceName));
            baseExtended.putItem(BaseExtended.RESPONSE_TYPE, actualTypeArguments[1]);
            baseExtended.putItem(BaseExtended.TARGET_MARK, Dict.CONSOLE_MARK);
            baseExtended.putItem(BaseExtended.SERVICE_MARK, serviceName);
            // request
            Map<String, Object> requestMap = new HashMap<>();
            for (ParamVariable paramVariable : paramVariableList) {
                requestMap.put(paramVariable.getVariableName(), paramVariable.getValue());
            }
            try {
                R result = requestWrapper.request(executor.executor(), requestMap);
                String resultStr = requestWrapper.resultString(result);
                log.debug("execute groupName: {}, commandName: {}, result: {}", groupName, commandName, result);
                OIConsole.console(resultStr);
            } catch (RemoteCallException e) {
                throw new InvokeException("call " + serviceName + " service failed");
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new InvokeException(e.getMessage());
            }
        }
    }

    public static class ParamVariable {
        private final String paramName;
        private final String variableName;
        private final Object value;

        public ParamVariable(String paramName, String variableName, Object value) {
            this.paramName = paramName;
            this.variableName = variableName;
            this.value = value;
        }

        @SuppressWarnings("unused")
        public String getParamName() {
            return paramName;
        }

        public String getVariableName() {
            return variableName;
        }

        public Object getValue() {
            return value;
        }
    }
}
